﻿using System;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Text;

namespace Microsoft.SqlCommunity.SQLRSS
{
    /// <summary>
    /// In order to generate a description of the database change for the RSS/Atom feed we need to combine constant text
    /// with data inserted from the database.  Allow a vaguely Format-like string which takes column names inside of square brackets where
    /// the column name gets substituted with the value of that column in the row that changed.  The string might look something like:
    /// "The price of {Name} is now {ListPrice}."  Spaces and punctuation are significant and only appear if the column value is non-null.  So
    /// "The operation {Operation} failed because of {Reason1}{, Reason2}" will not include the comma and space if the value 
    /// of the column Reason2 is null.
    /// 
    /// The parser is very simple.  It might be nice to add column name quoting with square brackets, and escaping of things like \tab and \{.
    /// For really fancy stuff we could make curly brackets recursive, but that probably isn't required.
    /// </summary>
    public class ParsedDescriptionFormat
    {
        // These are the so-called "psuedo-columns", the columns we get from change tracking.  Make them accessible to 
        // folks writing format strings but don't resolve them as if they were user columns
        public static readonly string[] PredefinedColumns = new string[] { "Operation", "OperationDate", "SYS_CHANGE_VERSION" };

        // Columns the user has specified in their format string.
        public List<string> ColumnNames { get; private set; }

        // A set of objects which represents the parse tree of the format string.  They are used to generate the output.
        public List<DescriptionFormatNode> FormatNodes { get; private set; }

        // The constructor for this class.
        public ParsedDescriptionFormat()
        {
            ColumnNames = new List<string>();
            FormatNodes = new List<DescriptionFormatNode>();
        }

        /// <summary>
        /// Initializes this object based on the specified format string, plus a qualifying function that helps us determine
        /// how to resolve column names since the columns could be from one of several tables.  The qualifying function will take a flat,
        /// possibly logical column name ("ProductName", for example) and map it to a schema qualified physical column name ("Production.Product.Name",
        /// for example).  Logical column names are defined in data views in the XML configuration file as DisplayName attributes of Column elements.
        /// </summary>
        /// <param name="formatString">a string with curly brackets around column names and accompanying punctuation (see above).</param>
        /// <param name="qualifyingFunction">a function which takes a flat, possibly logical column name and returns a 
        /// schema qualified physical column name</param>
        public void Parse(string formatString, Func<string,string> qualifyingFunction)
        {
            if (formatString == null) throw new ArgumentException("Format string must be specified.", "formatString");
            ColumnNames.Clear();
            FormatNodes.Clear();

            int start = 0;
            int end;
            VariableDescriptionFormatNode varNode;

            while (start < formatString.Length)
            {
                end = formatString.IndexOf('{', start);
                if (end < 0)
                {
                    FormatNodes.Add(new ConstantDescriptionFormatNode(formatString.Substring(start)));
                    break;
                }
                else
                {
                    if (end > start)
                    {
                        FormatNodes.Add(new ConstantDescriptionFormatNode(formatString.Substring(start, end - start)));
                        start = end;
                    }

                    //At this point, start should equals end, and be at the start of an expression bounded with curly brackets.
                    end = formatString.IndexOf('}', start);
                    if (end < 0)
                    {
                        throw new ArgumentException(string.Format("No close curly bracket for open curly bracket starting at {0} for description string {1}.",
                            start, formatString));
                    }
                    else
                    {
                        varNode = new VariableDescriptionFormatNode(formatString.Substring(start, (end - start) + 1),
                            qualifyingFunction);
                        //If this is the first time we've seen this column and it isn't a predefined column, then keep track of it.
                        if (!ColumnNames.Contains(varNode.ColumnName) 
                            && !(PredefinedColumns.Contains<string>(varNode.ColumnName, StringComparer.InvariantCultureIgnoreCase)))
                        {
                            ColumnNames.Add(varNode.ColumnName);
                        }
                        FormatNodes.Add(varNode);
                        //Be sure not to include close curly braket in constant data to be output
                        start = end + 1;
                    }
                }
            }

        }

        /// <summary>
        /// Send a column delimited set of column names to the string builder provided
        /// </summary>
        /// <param name="sb">Where to output the comma delimited string of column names</param>
        public void OutputCommaDelimitedColumnNames(StringBuilder sb)
        {
            Utility.OutputStringJoin<string>(", ", ColumnNames, (stringValue) => stringValue, sb);
        }

        /// <summary>
        /// Given a set of column values for each column used in the format expression, bind the variables in the format string
        /// to the column values, and return a string which contains the values substituted in for the format expressions.
        /// </summary>
        /// <param name="columnValues">A dictionary where the key is the name of the column and the value is the current value for the column</param>
        /// <returns>A filled in (bound) format string</returns>
        public string Bind(Dictionary<string, string> columnValues)
        {
            StringBuilder sb = new StringBuilder();

            foreach (DescriptionFormatNode node in FormatNodes)
            {
                node.Bind(columnValues, sb);
            }

            return sb.ToString();
        }

    }

    /// <summary>
    /// This class represents a parsed portion of a format string.
    /// </summary>
    public abstract class DescriptionFormatNode
    {
        public virtual void Bind(Dictionary<string, string> columnValues, StringBuilder sb)
        {
        }
    }

    /// <summary>
    /// This class represents a parsed portion of a format string which has only constant information in it.
    /// </summary>
    public class ConstantDescriptionFormatNode : DescriptionFormatNode
    {
        public ConstantDescriptionFormatNode(string descriptionText)
        {
            Description = descriptionText;
        }

        public string Description { get; private set; }

        public override void Bind(Dictionary<string, string> columnValues, StringBuilder sb)
        {
            //Column values are not used, they are just part of the protocol.
            sb.Append(Description);
        }
    }

    /// <summary>
    /// This class represents a parsed portion of a format string which contains variable information 
    /// (information that varies based on the value of a column)
    /// </summary>
    public class VariableDescriptionFormatNode : DescriptionFormatNode
    {
        // Any punctuation (or other non-constiuent) before the column value
        public ConstantDescriptionFormatNode PreNode { get; private set; }
        // The name of the column which contains the variable information
        public string ColumnName { get; private set; }
        // Any punctuation (or other non-constiuent) after the column value
        public ConstantDescriptionFormatNode PostNode { get; private set; }

        // Construct this node based on a section of the format string (starting at the open curly bracket) and
        // the qualifying function described above.
        public VariableDescriptionFormatNode(string descriptionText, Func<string,string> qualifyingFunction)
        {
            PreNode = null;
            PostNode = null;

            //Validate that we got reasonable input
            if (descriptionText[0] != '{' && descriptionText[descriptionText.Length - 1] != '}') 
                throw new ArgumentException(string.Format("Expecting variable description '{0}' to be surrounded by curly brackets",
                    descriptionText), "descriptionText");
            int varStart = -1;

            // Determine where the name of the variable starts and ends
            int varEnd = descriptionText.Length - 1;
            for (int i = 1; i < descriptionText.Length - 1; i++)
            {
                if (IsConstituent(descriptionText[i]))
                {
                    varStart = i;
                    break;
                }
            }

            if (varStart < 0) throw new ArgumentException(string.Format("Variable name missing from description string '{0}'.",
                descriptionText), "descriptionText");
            for (int i = varStart; i < descriptionText.Length - 1; i++)
            {
                if (!IsConstituent(descriptionText[i]))
                {
                    varEnd = i;
                    break;
                }
            }

            // If there is something before the start of the variable it must be some constant text we need to output iff 
            // the column value is not null.  Capture it and add it to the parse tree.
            if (varStart > 1) PreNode = new ConstantDescriptionFormatNode(descriptionText.Substring(1, varStart - 1));
            //Extract the qualified column name
            ColumnName = qualifyingFunction(descriptionText.Substring(varStart, varEnd - varStart));
            // If there is something after the start of the variable it must be some constant text we need to output iff 
            // the column value is not null.  Capture it and add it to the parse tree.
            if (varEnd < descriptionText.Length - 1) PostNode = new ConstantDescriptionFormatNode(descriptionText.Substring(varEnd, (descriptionText.Length - 1) - varEnd));
        }


        // Constituent characters are letters, digits, minus (-) or underscore (_).  Other punctuation and characters are
        // going to be interpreted as constants to be conditionally output if the column value for the row is not null.
        public bool IsConstituent(char characterToTest)
        {
            return char.IsLetterOrDigit(characterToTest) || characterToTest == '-' || characterToTest == '_';
        }

        // When serializing the parse tree with values bound, make sure to get the pre and post nodes of this node, as well as the 
        // value of the column.
        public override void Bind(Dictionary<string, string> columnValues, StringBuilder sb)
        {
            if (columnValues.ContainsKey(ColumnName))
            {
                if (PreNode != null) PreNode.Bind(columnValues, sb);
                sb.Append(columnValues[ColumnName]);
                if (PostNode != null) PostNode.Bind(columnValues, sb);
            }
        }
    }

}
